02 Dockerfile编写
可以通过手动进入容器安装依赖、修改配置,再使用docker commit保存结果。但这种方式难以重复,不便维护,也不利于审计。Dockerfile的价值在于把镜像构建过程写成文本文件,使不同人员和环境都能按照同一份定义构建出一致的镜像。
一、第一个Dockerfile
Dockerfile是一个纯文本文件,文件名固定为Dockerfile,没有扩展名。下面是一个最小但完整的示例:
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY src ./src
EXPOSE 8080
CMD ["python", "src/main.py"]各指令作用如下:
| 指令 | 作用 |
|---|---|
FROM python:3.12-alpine | 基础镜像,基于Python 3.12的Alpine版本 |
WORKDIR /app | 设置工作目录,后续命令都在这个目录下执行 |
COPY requirements.txt ./ | 把宿主机的文件复制到镜像里 |
RUN pip install ... | 在镜像里执行命令 |
EXPOSE 8080 | 声明容器监听的端口 |
CMD ["python", "src/main.py"] | 容器启动时执行的默认命令 |
构建镜像:
docker build -t my-agent:1.0 .其中,-t用于指定镜像名和标签,最后的.表示Dockerfile位于当前目录。
二、核心指令详解
2.1 FROM——选择基础镜像
每个Dockerfile都要从FROM开始,用它指定基础镜像。
FROM python:3.12-alpine
FROM node:22-alpine
FROM ubuntu:24.04选择基础镜像时,优先考虑以下原则:
| 原则 | 说明 |
|---|---|
| 用官方镜像 | Docker Hub上有Docker Official Images标记的 |
| 用小镜像 | Alpine版本比完整版小很多,python:3.12-alpine约50MB,python:3.12约1GB |
| 指定版本 | 不要用latest,明确版本号保证构建一致 |
2.2 WORKDIR——设置工作目录
WORKDIR /app后面的COPY、RUN、CMD等指令都会在这个目录下执行。如果目录不存在,Docker会自动创建。
应使用WORKDIR,而不是用RUN cd /app切换目录。 WORKDIR会影响后续所有指令,而RUN cd只在那一条RUN里有效。
2.3 COPY——复制文件
# 把宿主机的文件复制到镜像里
COPY requirements.txt ./
COPY src ./src
# 从其他构建阶段复制
COPY --from=builder /app/output ./COPY有两个参数:源路径在宿主机上,目标路径在镜像里。源路径是相对于Dockerfile所在目录来算的。
2.4 RUN——执行命令
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 安装系统包(Debian/Ubuntu)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*RUN会在镜像构建时执行,每一条RUN都会生成新的镜像层。多个命令能放在一起时,用&&连接可以减少层数。
2.5 ENV——设置环境变量
ENV PYTHONUNBUFFERED=1
ENV APP_PORT=8080通过ENV设置的环境变量在容器运行时可用,也可以在docker run时用-e覆盖。
2.6 EXPOSE——声明端口
EXPOSE 8080EXPOSE只是声明容器会监听哪个端口,并不会实际发布端口。外部访问服务时,还需要在docker run中使用-p做端口映射。
2.7 CMD——默认启动命令
CMD ["python", "src/main.py"]CMD指定容器启动时默认执行的命令。如果docker run时额外指定了命令,它会覆盖Dockerfile里的CMD。
建议使用JSON数组格式,也就是exec格式。 不要写成CMD python src/main.py这种shell格式。exec格式的进程能正确接收信号;shell格式会多包一层shell,信号传递容易出问题。
2.8 USER——指定运行用户
# 创建非root用户
RUN addgroup -S app && adduser -S app -G app
# 切换到非root用户
USER app容器默认用root运行,这会带来安全风险。 生产镜像应该创建并切换到非root用户。
2.9 ENTRYPOINT——入口点
ENTRYPOINT ["python", "src/main.py"]ENTRYPOINT和CMD类似,但ENTRYPOINT不会被docker run后面的参数直接覆盖。这些参数会被拼接到ENTRYPOINT后面。
常见的配合方式:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]运行docker run s3cmd时会显示帮助,运行docker run s3cmd ls s3://mybucket时则会执行实际命令。
三、.dockerignore文件
.dockerignore用于告诉Docker在构建镜像时忽略哪些文件,作用类似.gitignore。
.git
.env
__pycache__
*.pyc
node_modules
.venv
*.md.dockerignore主要有三个作用:
- 减小构建上下文——Docker会把当前目录的文件发送给Daemon,排除无关文件后传输更快
- 避免敏感信息泄露——
.env文件里可能有API Key,不应该被打进镜像 - 避免缓存失效——
node_modules这类目录变化频繁,排除后不会因为它们变动导致重建
四、构建最佳实践
4.1 利用构建缓存
Docker会缓存每一层的构建结果。某一层的输入没变时,Docker会直接复用缓存,跳过执行。
关键思路是:把变化频率低的指令放前面,变化频率高的放后面。
# 差的写法——代码一改,依赖全重装
COPY . .
RUN pip install -r requirements.txt
# 好的写法——先装依赖,再复制代码
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .先复制requirements.txt安装依赖,再复制整个项目。仅修改代码时,依赖安装这一层仍可命中缓存,不需要每次重新安装依赖。
4.2 合并RUN指令
每条RUN都会生成一个层。层数增加会带来额外开销,因此可以合并的命令应尽量合并:
# 差的写法——三层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 好的写法——一层
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*apt-get update和apt-get install要放在同一条RUN里。如果分开写,apt-get update一旦被缓存,后面的apt-get install可能会拿到过期的包索引。
4.3 用Alpine镜像
Alpine Linux是一个极简的Linux发行版,基础镜像只有几MB。大多数官方镜像也都提供Alpine版本:
FROM python:3.12-alpine # ~50MB
FROM node:22-alpine # ~180MB
FROM nginx:alpine # ~40MB和动辄几百MB甚至1GB的完整版相比,Alpine能明显减小镜像体积。不过它用apk而不是apt-get安装包,用musl而不是glibc,少数情况下会遇到兼容问题。
4.4 不要安装不必要的包
镜像中安装的每个包都会带来额外体积和潜在风险。数据库镜像不需要文本编辑器,Web服务镜像也不应包含不必要的编译工具。
五、完整的Python Agent Dockerfile
结合前面的原则,一个更接近生产使用的Agent服务Dockerfile如下:
FROM python:3.12-alpine
# 创建非root用户
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# 先复制依赖文件,利用缓存
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# 再复制源代码
COPY src ./src
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 切换到非root用户
USER app
# 声明端口
EXPOSE 8080
# 启动命令
CMD ["python", "src/main.py"]六、总结
Dockerfile中常用指令如下:
| 指令 | 作用 | 执行时机 |
|---|---|---|
FROM | 基础镜像 | 构建时 |
WORKDIR | 工作目录 | 构建时 |
COPY | 复制文件 | 构建时 |
RUN | 执行命令 | 构建时 |
ENV | 环境变量 | 运行时 |
EXPOSE | 声明端口 | 运行时 |
CMD | 默认命令 | 运行时 |
USER | 运行用户 | 运行时 |
编写Dockerfile时,可以遵循以下原则:
- 层序优化——变化少的指令放前面,变化多的放后面
- 层合并——能合并的
RUN就合并,减少层数 - 最小化——用Alpine基础镜像、不装多余包、非root运行
镜像分层机制和构建缓存会直接影响构建速度、镜像体积和维护成本,是Dockerfile优化的基础。